package com.example.weixinpayment.service.impl;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.example.weixinpayment.config.WxPayConfig;
import com.example.weixinpayment.constant.enums.OrderStatus;
import com.example.weixinpayment.constant.enums.wxpay.WxApiType;
import com.example.weixinpayment.constant.enums.wxpay.WxNotifyType;
import com.example.weixinpayment.constant.enums.wxpay.WxRefundStatus;
import com.example.weixinpayment.constant.enums.wxpay.WxTradeState;
import com.example.weixinpayment.entity.OrderInfo;
import com.example.weixinpayment.entity.RefundInfo;
import com.example.weixinpayment.result.ResultCode;
import com.example.weixinpayment.service.OrderInfoService;
import com.example.weixinpayment.service.PaymentInfoService;
import com.example.weixinpayment.service.RefundInfoService;
import com.example.weixinpayment.service.WxPayService;
import com.example.weixinpayment.util.OrderNoUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @program: weixin-payment
 * @description: 微信支付service实现
 * @author: LiuZhuzheng
 * @create: 2021-12-29 16:42
 **/
@Slf4j
@Service
public class WxPayServicesImpl implements WxPayService {

    @Autowired
    private WxPayConfig wxPayConfig;

    /**
     * WxPayConfig中配置bean
     * 经过微信支付平台签名和验签的httpClient
     * */
    @Autowired
    private CloseableHttpClient wxPayClient;

    @Autowired
    private CloseableHttpClient wxPayNoSignClient;

    @Autowired
    private OrderInfoService orderInfoService;

    @Autowired
    private PaymentInfoService paymentInfoService;

    @Autowired
    private RefundInfoService refundInfoService;

    private final ReentrantLock lock = new ReentrantLock();


    @Override
    public Map<String, Object> nativePay(Long productId) throws Exception{
        log.info("begin generate order, the productId is {}", productId);

        OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId);
        String codeUrl = orderInfo.getCodeUrl();
        String orderNo = orderInfo.getOrderNo();
        //如果二维码存在且未过时
        if (ObjectUtil.isNotEmpty(orderInfo) && StringUtils.isNotBlank(codeUrl)
                && LocalDateTime.ofInstant(orderInfo.getCodeUrlExpireTime().toInstant(), ZoneId.systemDefault()).isAfter(LocalDateTime.now())){
            //最终返回结果封装
            Map<String, Object> result = new HashMap<>();
            result.put("codeUrl", codeUrl);
            result.put("orderNo", orderNo);

            return result;
        }

        log.info("Start placing orders and call wechat Payment platform to generate two-dimensional code");

        //调用微信支付平台统一下单api
        HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));

        //body参数
        Map<String, Object> params = new HashMap<>();
        params.put("appid", wxPayConfig.getAppid());
        params.put("mchid", wxPayConfig.getMchId());
        params.put("description", orderInfo.getTitle());
        params.put("out_trade_no", orderInfo.getOrderNo());
        params.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));
        Map<String, Object> amountParam = new HashMap<>();
        amountParam.put("total", orderInfo.getTotalFee());
        amountParam.put("currency", "CNY");
        params.put("amount", amountParam);

        //转为json对象
        ObjectMapper objectMapper = new ObjectMapper();
        String json;
        try {
            json = objectMapper.writeValueAsString(params);
        } catch (JsonProcessingException e) {
            log.error("jackson map to json fail, params:{} -> e:{}", params, e);
            throw new RuntimeException("jackson map to json fail");
        }

        StringEntity entity = new StringEntity(json, StandardCharsets.UTF_8);
        entity.setContentType(ContentType.APPLICATION_JSON.toString());
        httpPost.setEntity(entity);
        httpPost.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());

        //完成签名发送请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            String body = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();

            //处理成功
            if (statusCode == ResultCode.OK.getCode()) {
                log.info("Place an order successfully, body:{}", body);
            } else if (statusCode == ResultCode.NO_COMMENT.getCode()) {
                //处理成功没有返回body
                log.info("Place an order successfully");
            } else {
                //处理失败
                log.info("Place an order fail, statusCode:{}, body:{}", statusCode, body);
                throw new RuntimeException("wechat payment platform failed to place an order");
            }

            HashMap<String, String> resultMap = objectMapper.readValue(body, HashMap.class);
            //支付二维码
            codeUrl = resultMap.get("code_url");
            orderNo = orderInfo.getOrderNo();
            orderInfoService.updateCodeUrl(orderNo, codeUrl);

            //最终返回结果封装
            Map<String, Object> result = new HashMap<>();
            result.put("codeUrl", codeUrl);
            result.put("orderNo", orderNo);

            return result;
        } catch (IOException e) {
            log.error("wechat payment platform failed to place an order, httpPost:{} -> exception:{}", httpPost, e);
            throw new RuntimeException("wechat payment platform failed to place an order");
        } finally {
            response.close();
        }
    }

    @Override
    public void processOrder(Map<String, Object> body) {
        log.info("process order ......");

        String plainText = decryptFromResource(body);

        //明文转换成map
        HashMap<String, Object> plainTextMap = JSONObject.parseObject(plainText, HashMap.class);
        //商户订单号
        String orderNo = (String) plainTextMap.get("out_trade_no");

        //在对业务数据进行状态检查和处理之前，要采用数据锁进行并发控制，以避免函数重入造成的数据混乱。
        //与synchronized区别是 synchronized 未获取到锁会一直等待 ReentrantLock未获取到锁会立即返回false
        if (lock.tryLock()){
            try {
                //处理微信重复通知
                String status = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.NOTPAY.getType().equals(status)){
                    return;
                }

                //更新订单状态
                orderInfoService.updateOrderStatus(orderNo, OrderStatus.SUCCESS);

                //记录支付日志
                paymentInfoService.createPaymentInfo(plainText);
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }

    @Override
    public void cancelOrder(String orderNo) throws Exception {
        // 调用微信支付关单接口
        this.closeOrder(orderNo);

        // 更新商户端订单状态
        orderInfoService.updateOrderStatus(orderNo, OrderStatus.CANCEL);
    }

    @Override
    public String queryOrder(String orderNo) throws Exception{
        log.info("query order , the orderNo is {}", orderNo);

        // 创建http请求对象
        String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());
        HttpGet httpGet = new HttpGet(url);

        httpGet.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());

        //完成签名发送请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);
        try {
            String body = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();

            //处理成功
            if (statusCode == ResultCode.OK.getCode()) {
                log.info("Place an order successfully, body is {}", body);
            } else if (statusCode == ResultCode.NO_COMMENT.getCode()) {
                //处理成功没有返回body
                log.info("Place an order successfully");
            } else {
                //处理失败
                log.info("Place an order fail, statusCode:{}", statusCode);
                throw new RuntimeException("wechat payment platform failed to place an order");
            }
            return body;
        } catch (IOException e) {
            log.error("wechat payment platform failed to query an order, httpGet:{} -> exception:{}", httpGet, e);
            throw new RuntimeException("wechat payment platform query to place an order");
        } finally {
            response.close();
        }
    }

    /**
     * 如果订单已支付，则更新商户端订单状态
     * 如果订单未支付，则调用微信端关单接口并更新商户端订单状态
     * */
    @Override
    public void checkOrderStatus(String orderNo) throws Exception {
        log.warn("verify order status according to order number, the orderNo is {}", orderNo);

        // 调用微信端查单接口
        String order = this.queryOrder(orderNo);
        HashMap<String, Object> resultMap = JSONObject.parseObject(order, HashMap.class);

        //获取微信端订单状态
        String tradeState = (String) resultMap.get("trade_state");

        //判断订单状态
        if (WxTradeState.SUCCESS.getType().equals(tradeState)){
            log.info("Verify that the order has been paid, the orderNo is {}", orderNo);

            //更新本地订单状态未已支付
            orderInfoService.updateOrderStatus(orderNo, OrderStatus.SUCCESS);
            //记录支付日志
            paymentInfoService.createPaymentInfo(order);
        }

        if (WxTradeState.NOTPAY.getType().equals(tradeState)){
            log.warn("Verify that the order not paid, the orderNo is {}", orderNo);

            //调用关单接口
            this.cancelOrder(orderNo);

            //更新本地订单状态
            orderInfoService.updateOrderStatus(orderNo, OrderStatus.CLOSED);
        }
    }

    @Override
    public void refund(String orderNo, String reason) throws Exception{
        log.info("Create a record of refund receipts");
        //根据订单编号创建退款单
        RefundInfo refundsInfo = refundInfoService.createRefundByOrderNo(orderNo, reason);

        log.info("Calling the Refund API");
        String url = wxPayConfig.getDomain().concat(WxApiType.DOMESTIC_REFUNDS.getType());
        HttpPost httpPost = new HttpPost(url);

        //请求body参数
        Map<String, Object> params = new HashMap<>();
        //订单编号
        params.put("out_trade_no", orderNo);
        //退款单编号
        params.put("out_refund_no", refundsInfo.getRefundNo());
        //退款原因
        params.put("reason",reason);
        //退款通知地址
        params.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.REFUND_NOTIFY.getType()));

        Map<String, Object> amountMap = new HashMap<>();
        //退款金额
        amountMap.put("refund", refundsInfo.getRefund());
        //原订单金额
        amountMap.put("total", refundsInfo.getTotalFee());
        //退款币种
        amountMap.put("currency", "CNY");
        params.put("amount", amountMap);

        //将参数转成json字符串
        String paramsJson = JSONObject.toJSONString(params);
        log.info("request params : {}", paramsJson);

        StringEntity entity = new StringEntity(paramsJson,"utf-8");
        //设置请求报文格式
        entity.setContentType("application/json");
        //将请求报文放入请求对象
        httpPost.setEntity(entity);
        //设置响应报文格式
        httpPost.setHeader("Accept", "application/json");

        //完成签名并执行请求，并完成验签
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            //解析响应结果
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("退款异常, 响应码 = " + statusCode+ ", 退款返回结果 = " + bodyAsString);
            }

            //更新订单状态
            orderInfoService.updateOrderStatus(orderNo, OrderStatus.REFUND_PROCESSING);

            //更新退款单
            refundInfoService.updateRefund(bodyAsString);
        } catch (Exception e) {
            log.error("wechat payment platform failed to refund an order, httpPost:{} -> exception:{}", httpPost, e);
            throw new RuntimeException("wechat payment platform failed to refund an order");
        } finally {
            response.close();
        }
    }

    @Override
    public void processRefund(Map<String, Object> bodyMap) {
        log.info("Refund single");

        //解密报文
        String plainText = decryptFromResource(bodyMap);

        //将明文转换成map
        HashMap<String, Object> plainTextMap = JSONObject.parseObject(plainText, HashMap.class);
        String orderNo = (String)plainTextMap.get("out_trade_no");

        if(lock.tryLock()){
            try {

                String orderStatus = orderInfoService.getOrderStatus(orderNo);
                if (!OrderStatus.REFUND_PROCESSING.getType().equals(orderStatus)) {
                    return;
                }

                //更新订单状态
                orderInfoService.updateOrderStatus(orderNo, OrderStatus.REFUND_SUCCESS);

                //更新退款单
                refundInfoService.updateRefund(plainText);

            } finally {
                //要主动释放锁
                lock.unlock();
            }
        }
    }

    @Override
    public String queryRefund(String refundNo) throws Exception {
        log.info("query refund, the refundNo is {}", refundNo);

        String url =  String.format(WxApiType.DOMESTIC_REFUNDS_QUERY.getType(), refundNo);
        url = wxPayConfig.getDomain().concat(url);

        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.setHeader("Accept", "application/json");

        //完成签名并执行请求
        CloseableHttpResponse response = wxPayClient.execute(httpGet);
        try {
            String bodyAsString = EntityUtils.toString(response.getEntity());
            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 查询退款返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("查询退款异常, 响应码 = " + statusCode+ ", 查询退款返回结果 = " + bodyAsString);
            }

            return bodyAsString;

        } catch (Exception e) {
            log.error("wechat payment platform failed to query an refund, httpPost:{} -> exception:{}", httpGet, e);
            throw new RuntimeException("wechat payment platform failed to query an refund");
        } finally {
          response.close();
        }
    }

    @Override
    public void checkRefundStatus(String refundNo) throws Exception{
        log.warn("Verify the status of refund receipt according to the refundNo: {}", refundNo);

        //调用查询退款单接口
        String result = this.queryRefund(refundNo);

        //组装json请求体字符串
        Map<String, String> resultMap = JSONObject.parseObject(result, HashMap.class);

        //获取微信支付端退款状态
        String status = resultMap.get("status");

        String orderNo = resultMap.get("out_trade_no");

        if (WxRefundStatus.SUCCESS.getType().equals(status)) {

            log.warn("Verify that the order has been refunded successfully, refundNo is {}", refundNo);

            //如果确认退款成功，则更新订单状态
            orderInfoService.updateOrderStatus(orderNo, OrderStatus.REFUND_SUCCESS);

            //更新退款单
            refundInfoService.updateRefund(result);
        }

        if (WxRefundStatus.ABNORMAL.getType().equals(status)) {

            log.warn("Verify the order refund exception, refundNo is {}", refundNo);

            //如果确认退款成功，则更新订单状态
            orderInfoService.updateOrderStatus(orderNo, OrderStatus.REFUND_ABNORMAL);

            //更新退款单
            refundInfoService.updateRefund(result);
        }
    }

    @Override
    public String queryBill(String billDate, String type) throws Exception{
        log.warn("Request billing interface call，billDate is {}", billDate);

        String url = "";
        if("tradebill".equals(type)){
            url =  WxApiType.TRADE_BILLS.getType();
        }else if("fundflowbill".equals(type)){
            url =  WxApiType.FUND_FLOW_BILLS.getType();
        }else{
            throw new RuntimeException("不支持的账单类型");
        }

        url = wxPayConfig.getDomain().concat(url).concat("?bill_date=").concat(billDate);

        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(url);
        httpGet.addHeader("Accept", "application/json");

        //使用wxPayClient发送请求得到响应
        CloseableHttpResponse response = wxPayClient.execute(httpGet);

        try {

            String bodyAsString = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 申请账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("申请账单异常, 响应码 = " + statusCode+ ", 申请账单返回结果 = " + bodyAsString);
            }

            //获取账单下载地址
            Map<String, String> resultMap = JSONObject.parseObject(bodyAsString, HashMap.class);
            return resultMap.get("download_url");

        } finally {
            response.close();
        }
    }

    @Override
    public String downloadBill(String billDate, String type) throws Exception{
        log.warn("download billing interface call，billDate is {}, type is {}", billDate, type);

        //获取账单url地址
        String downloadUrl = this.queryBill(billDate, type);
        //创建远程Get 请求对象
        HttpGet httpGet = new HttpGet(downloadUrl);
        httpGet.addHeader("Accept", "application/json");

        //使用wxPayClient发送请求得到响应
        CloseableHttpResponse response = wxPayNoSignClient.execute(httpGet);

        try {

            String bodyAsString = EntityUtils.toString(response.getEntity());

            int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode == 200) {
                log.info("成功, 下载账单返回结果 = " + bodyAsString);
            } else if (statusCode == 204) {
                log.info("成功");
            } else {
                throw new RuntimeException("下载账单异常, 响应码 = " + statusCode+ ", 下载账单返回结果 = " + bodyAsString);
            }

            return bodyAsString;

        } finally {
            response.close();
        }
    }

    /**
     * @description: 微信端关单接口的调用
     * @param: orderNo
     * @return: void
     * @author: 刘助政
     * @date: 2022/1/11 17:46
     * */
    private void closeOrder(String orderNo) throws Exception {
        log.info("Call off the order interface, the order number is {}", orderNo);

        // 创建http请求对象
        String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);
        url = wxPayConfig.getDomain().concat(url);
        HttpPost httpPost = new HttpPost(url);

        // 组装json请求体
        Map<String, String> params = new HashMap<>();
        params.put("mchid", wxPayConfig.getMchId());
        String jsonParam = JSONObject.toJSONString(params);

        StringEntity entity = new StringEntity(jsonParam, StandardCharsets.UTF_8);
        entity.setContentType(ContentType.APPLICATION_JSON.toString());
        httpPost.setEntity(entity);
        httpPost.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString());

        //完成签名发送请求
        CloseableHttpResponse response = wxPayClient.execute(httpPost);
        try {
            int statusCode = response.getStatusLine().getStatusCode();

            //处理成功
            if (statusCode == ResultCode.OK.getCode()) {
                log.info("Place an order successfully");
            } else if (statusCode == ResultCode.NO_COMMENT.getCode()) {
                //处理成功没有返回body
                log.info("Place an order successfully");
            } else {
                //处理失败
                log.info("Place an order fail, statusCode:{}", statusCode);
                throw new RuntimeException("wechat payment platform failed to place an order");
            }
        } catch (Exception e) {
            log.error("wechat payment platform failed to place an order, httpPost:{} -> exception:{}", httpPost, e);
            throw new RuntimeException("wechat payment platform failed to place an order");
        } finally {
            response.close();
        }
    }

    /**
     * @description: 对称解密
     * @param: body
     * @return: java.lang.String
     * @author: 刘助政
     * @date: 2022/1/11 16:04
     * */
    private String decryptFromResource(Map<String, Object> body) {
        log.info("symmetric decryption of ciphertext ......");

        //通知数据
        Map<String, String> resource = (Map) body.get("resource");
        //数据密文
        String ciphertext = resource.get("ciphertext");
        //附加数据
        String associatedData = resource.get("associated_data");
        //随机串
        String nonce = resource.get("nonce");

        AesUtil aesUtil = new AesUtil(wxPayConfig.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        String plainText = "";
        try {
            log.info("ciphertext:{}", ciphertext);
            plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                    nonce.getBytes(StandardCharsets.UTF_8),
                    ciphertext);
            log.info("plainText:{}", plainText);
        } catch (GeneralSecurityException e) {
            log.error("aesUtil.decryptToString fail ,resource:{} -> error:{}", resource, e);
        }
        return plainText;
    }
}
